package lion.framework.core.conf.loader;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

import lion.dev.lang.MapJ;
import lion.dev.lang.XMLUtil;
import lion.framework.core.conf.Config;
import lion.framework.core.conf.IConfigLoader;
import lion.framework.core.conf.anno.ConfigLoader;

/**
 * 框架配置加载器，读取classpath 下面的.xml文件，<br/>
 * 访问以 root.node1.node2的形式进行访问，<br/>
 * 如果元素只有一个返回一个Map,如果是多个元素，则返回一个map列表，在取值的时候需要作好判断<br/>
 * note:<br/>
 * 1、在framework.xml 中可以加入自定义的属性，以上面提到的方式进行访问。<br/>
 * 2、元素节点只有两种形式，一种是只有属性，读取的配置将会是一个属性Map，另一种是只有文本节点。<strong>不能两种方式同时存在。</strong>
 * 3、节点路径是唯一的，如
 *
 * <pre>
 * 	<book>
 * 		<name>Thinking in Java</name>
 * 		<price>56</price>
 * 	</book>
 * 	<book>
 * 		<name>Thinking in C++</name>
 * 		<price>56</price>
 * 	</book>
 * 	的形式将不被支持，我们可以将其改成：
 * 	<book name="Thinking in Java" price="56" />
 * 	<book name="Thinking in C++" price="56" />
 * </pre>
 *
 * <pre>
 * time   2013年8月5日
 * mail   hl_0074@sina.com
 * </pre>
 *
 * @author lion
 */
@ConfigLoader("xml")
public class XMLConfigLoader implements IConfigLoader {

	private static Log log = LogFactory.getLog(XMLConfigLoader.class);

	private Document doc;
	private final String path;

	public XMLConfigLoader(MapJ param) {

		this(param.getString("file"));
	}

	public XMLConfigLoader(String path) {

		this.path = path;
	}

	@Override
	public Map<String, Object> load() throws Exception {

		Map<String, Object> result = new HashMap<>();

		SAXReader saxReader = new SAXReader();
		InputStream is = this.getClass().getClassLoader().getResourceAsStream(path);
		if (is == null) {
			if (log.isErrorEnabled()) {
				log.error("config file " + path + " not found ");
				return result;
			}
		}
		try {
			doc = saxReader.read(is);
		} finally {
			IOUtils.closeQuietly(is);
		}
		readElement(doc.getRootElement(), result);

		return result;
	}

	@Override
	public void save(Config conf) throws Exception {

		doc = DocumentHelper.createDocument();

		String key = conf.getKey(0);
		String root = key.substring(0, key.indexOf("."));
		doc.addElement(root);
		for (String name : conf.keySet()) {
			addElement(name, conf.get(name));
		}
		saveDoc();
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T select(String arg) {

		if (StringUtils.isBlank(arg)) { return null; }

		return (T) doc.selectNodes(arg);
	}

	@SuppressWarnings("rawtypes")
	private void addElement(String path, Object value) {

		if (StringUtils.isBlank(path) || value == null) { return; }

		if (!path.startsWith("/")) {
			path = "/" + path;
		}
		path = path.replace(".", "/");
		if (path.endsWith("/")) {
			path = path.substring(0, path.length() - 1);
		}

		if (List.class.isAssignableFrom(value.getClass())) {
			for (Object o : (List) value) {
				buildElement(o, getNode(path));
			}
		} else {
			buildElement(value, getNode(path));
		}
	}

	@SuppressWarnings("unchecked")
	private void buildElement(Object value, Element e) {

		if (Map.class.isAssignableFrom(value.getClass())) {
			Map<String, Object> attrMap = (Map<String, Object>) value;
			for (String attr : attrMap.keySet()) {
				Object v = attrMap.get(attr);
				if (v == null) {
					continue;
				}
				e.addAttribute(attr, String.valueOf(v));
			}
		} else {
			e.setText(String.valueOf(value));
		}
	}

	private Element getNode(String path) {

		Element parent = doc.getRootElement();
		String[] arr = StringUtils.split(path, "/");
		for (int i = 1; i < arr.length - 1; i++) {
			Element sub = parent.element(arr[i]);
			if (sub == null) {
				sub = parent.addElement(arr[i]);
			}
			parent = sub;
		}
		return parent.addElement(arr[arr.length - 1]);
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private void readElement(Element element, Map<String, Object> result) {

		Object value = null;
		if (element.attributeCount() > 0) {
			value = XMLUtil.attributeMap(element);
		} else {
			String text = element.getTextTrim();
			if (StringUtils.isNotBlank(text)) {
				value = text;
			}
		}
		if (value != null) {
			String path = element.getPath().substring(1).replace("/", ".");
			Object old = result.get(path);
			if (old == null) {
				result.put(path, value);
			} else {
				if (List.class.isAssignableFrom(old.getClass())) {
					((List) old).add(value);
				} else {
					List t = new ArrayList();
					t.add(old);
					t.add(value);
					result.put(path, t);
				}
			}
		}

		for (Element subele : (List<Element>) element.elements()) {
			readElement(subele, result);
		}
	}

	private void saveDoc() throws Exception {

		OutputFormat format = OutputFormat.createPrettyPrint();
		format.setTrimText(true);
		format.setIndent(true);
		format.setIndentSize(4);
		format.setEncoding("UTF-8");
		XMLWriter writer = null;
		try {
			writer = new XMLWriter(new FileOutputStream(this.getClass().getClassLoader().getResource(path).getPath()), format);
			writer.write(doc);
		} catch (Exception e) {
		} finally {
			if (writer != null) {
				writer.close();
			}
		}
	}
}
